﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml;
using System.Diagnostics;

namespace Douban.Api.Infrastructure
{
    /// <summary>
    /// This is the base class for all the xml element
    /// </summary>
    public abstract class Element
    {
        /// <summary>
        /// This holds the xml node corresponds to the element
        /// </summary>
        protected XmlNode RootNode { get; set; }


        protected static XmlNamespaceManager NamespaceManager { get; set; }

        /// <summary>
        /// Derived class should override this method to provider extra initialization not 
        /// covered by base class
        /// </summary>
        protected abstract void Initialize();

        /// <summary>
        /// Should call this method to do initialization
        /// </summary>
        /// <param name="relativeRoot">The xml node corresponds to the element</param>
        public void Initialize(XmlNode relativeRoot)
        {
            if (relativeRoot == null)
                throw new ArgumentNullException("relativeRoot");

            RootNode = relativeRoot;

            InitializeNamespaces();
            Initialize();
        }

        /// <summary>
        /// Resolve the realated XmlNode and deserialize xml into correct properties
        /// </summary>
        public void Resolve()
        {
            IEnumerable<PropertyInfo> propInfos = GetType().GetProperties();

            foreach (var propInfo in propInfos.Where(p => Attribute.IsDefined(p, typeof(ElementAttribute))))
            {
                ElementAttribute elementAttribute =
                        (ElementAttribute)Attribute.GetCustomAttribute(propInfo, typeof(ElementAttribute));

                string xpath = elementAttribute.GetXPath(NamespaceManager);

                if (elementAttribute is ElementCollectionAttribute)
                {
                    Type elementType = propInfo.PropertyType;

                    VerifyElementCollectionType(elementType);
                    object collection = GetCollection(elementType);
                    MethodInfo addMethod = collection.GetType().GetMethod("Add");

                    XmlNodeList nodeList = RootNode.SelectNodes(xpath, NamespaceManager);

                    //if (nodeList.Count == 0) continue;

                    foreach (XmlNode node in nodeList)
                        addMethod.Invoke(
                            collection, 
                            new [] { GetNodeValue(elementType.GetGenericArguments()[0], node) });

                    propInfo.SetValue(this, collection, null);

                    continue;
                }
                
                propInfo.SetValue(this, GetNodeValue(propInfo.PropertyType, xpath), null);
            }
        }

        private object GetNodeValue(Type targetType, string xpath)
        {
            return GetNodeValue(targetType, RootNode.SelectSingleNode(xpath, NamespaceManager));
        }

        private object GetNodeValue(Type targetType, XmlNode node)
        {
            object value = null;

            if (node == null) return null;

            if (targetType.IsSubclassOf(typeof(Element)))
                value = GetElement(node, targetType);
            else
            {
                value = (targetType == typeof(string))
                            ? node.InnerText
                            : Convert.ChangeType(node.InnerText, targetType);
            }

            return value;
        }

        private Element GetElement(XmlNode node, Type type)
        {
            Element element = (Element)Activator.CreateInstance(type);
            element.Initialize(node);
            element.Resolve();

            return element;
        }

        private void VerifyElementCollectionType(Type type)
        {
            Type expectedCollectionType = typeof (IList<>).MakeGenericType(type.GetGenericArguments()[0]);

            if (!expectedCollectionType.IsAssignableFrom(type))
                throw new InvalidElementCollectionException(
                    "Element collection should be a concrete type of " +
                    expectedCollectionType);
        }

        private object GetCollection(Type type)
        {
            return Activator.CreateInstance(typeof(List<>).MakeGenericType(type.GetGenericArguments()[0]));
        }

        private void InitializeNamespaces()
        {
            if (NamespaceManager != null) return;

            NamespaceManager = new XmlNamespaceManager(RootNode.OwnerDocument.NameTable);
            NamespaceManager.AddNamespace("default", NsManager.Default);
            NamespaceManager.AddNamespace("gd", NsManager.GoogleData);
            NamespaceManager.AddNamespace("os", NsManager.OpenSearch);
            NamespaceManager.AddNamespace("db", NsManager.Douban);
        }
    }
}
